#!/usr/bin/python3
import tkinter as tk
import requests
import time
import vlc
import os
import random
import threading
import re
import json
from pathlib import Path
import numpy as np, sounddevice as sd
import asyncio
from bilibili_api import live, sync
import queue
music_folders = [
	"D:\\user\\Desktop\\music",
	"/mnt/file/document/media/music",
	"C:/Users/Administrator/Music/music"
]
IS_PLAYING_LOCAL_MUSIC = True
global_gui = None
def find_config_file():
	possible_paths = [
		'bili-song-requests.json',
		os.path.expanduser('~/bili-song-requests.json'),
		os.path.join(os.path.dirname(__file__), 'bili-song-requests.json'),
	]
	for path in possible_paths:
		if os.path.exists(path):
			print(f"找到配置文件: {path}")
			return path
	raise FileNotFoundError("未找到配置文件")
danmaku_queue = queue.Queue()
class Watchdog:
	def __init__(self, player_ref):
		self.player_ref = player_ref
		self.running = True
		self.last_position = 0
		self.last_position_time = 0
		self.stuck_count = 0
		self.max_stuck_count = 3
	def start(self):
		def watchdog_worker():
			while self.running:
				try:
					time.sleep(5)
					if not self.running:
						break
					self._check_music_status()
				except Exception as e:
					global_gui.set_status(system_status="[看门狗🐶]💀哎呀！狗摔跤了！")
					sd.play(np.sign(np.sin(2*np.pi*1000*np.arange(44100)/44100)), 44100)
					time.sleep(3)
					continue
		self.thread = threading.Thread(target=watchdog_worker, daemon=True)
		self.thread.start()
	def _check_music_status(self):
		try:
			current_state = self.player_ref.player.get_state()
			current_position = self.player_ref.player.get_time()
			current_time = time.time()
			should_skip = False
			if current_state == vlc.State.Paused:
				global_gui.set_status(system_status="[看门狗🐶]暂停了？咬！")
				should_skip = True
			elif current_state == vlc.State.Error:
				global_gui.set_status(system_status="[看门狗🐶]嗷！播放器出错了，狠狠咬！")
				should_skip = True
			elif current_state == vlc.State.Playing:
				if current_position == self.last_position:
					if self.last_position_time == 0:
						self.last_position_time = current_time
					else:
						if current_time - self.last_position_time > 15:
							self.stuck_count += 1
							global_gui.set_status(system_status=f"[看门狗🐶]咬住不放({self.stuck_count}/3)")
							if self.stuck_count >= self.max_stuck_count:
								should_skip = True
								self.stuck_count = 0
				else:
					self.last_position = current_position
					self.last_position_time = current_time
					self.stuck_count = 0
			elif current_state in [vlc.State.Stopped, vlc.State.NothingSpecial, vlc.State.Ended]:
				pass
			else:
				global_gui.set_status(system_status="[看门狗🐶]❓汪？发生了什么？不知道！咬一口！")
				should_skip = True
			if should_skip:
				sd.play(np.sign(np.sin(2*np.pi*1000*np.arange(44100)/44100)), 44100)
				final_check = self.player_ref.player.get_state()
				if final_check in [vlc.State.Paused, vlc.State.Error] or \
				   (final_check == vlc.State.Playing and self.player_ref.player.get_time() == current_position):
					self.player_ref.player.stop()
					self.last_position = 0
					self.last_position_time = 0
					self.stuck_count = 0
		except Exception as e:
			global_gui.set_status(system_status="[看门狗🐶]🎵音乐状态检查失败")
			sd.play(np.sign(np.sin(2*np.pi*1000*np.arange(44100)/44100)), 44100)
	def stop(self):
		self.running = False
class PointSongGUI:
	def __init__(self):
		global global_gui
		self.root = tk.Tk()
		self.root.configure(bg='#000000')
		self.root.overrideredirect(True)
		self.root.bind('<Button-1>', lambda e: [setattr(self, 'x', e.x), setattr(self, 'y', e.y)])
		self.root.bind('<B1-Motion>', lambda e: self.root.geometry(f"+{self.root.winfo_x() + e.x - self.x}+{self.root.winfo_y() + e.y - self.y}"))
		self.root.bind('<p>', self.pause_music)
		self.root.bind('<q>', self.skip_song)
		self.main_frame = tk.Frame(self.root, bg='#000000', padx=0, pady=0)
		self.main_frame.pack(fill=tk.BOTH, expand=True, padx=0, pady=0)
		self.status_label = tk.Label(self.main_frame, text="🏫考场解封中...", font=("", 16), foreground="#808080", background="#000000", justify=tk.LEFT, anchor='w')
		self.fixed_label = tk.Label(self.main_frame, text="📝 发送「点歌 歌名」或「切歌」或「测试」", font=("", 12), foreground="#008000", background="#000000", justify=tk.LEFT, anchor='w')
		self.danmu_label = tk.Label(self.main_frame, text="暂无最新弹幕", font=("", 12), foreground="#008080", background="#000000", justify=tk.LEFT, anchor='w')
		self.system_status_label = tk.Label(self.main_frame, text="📊 考场状态:🔒未解封", font=("", 12), foreground="#808000", background="#000000", justify=tk.LEFT, anchor='w')
		self.status_label.pack(padx=0, pady=0, fill=tk.X)
		self.fixed_label.pack(padx=0, pady=0, fill=tk.X)
		self.danmu_label.pack(padx=0, pady=0, fill=tk.X)
		self.system_status_label.pack(padx=0, pady=0, fill=tk.X)
		self.current_song = ""
		self.next_song = ""
		self.queue_count = 0
		self.last_song = ""
		self.system_status = ""
		self.root.geometry(f"+{50}+{50}")
		global_gui = self
	def skip_song(self, e=None):
		if hasattr(self, 'player_ref') and self.player_ref:
			self.player_ref.player.stop()
			self.set_status(system_status="⏭️手动切歌")
	def pause_music(self, e=None):
		if hasattr(self, 'player_ref') and self.player_ref:
			self.player_ref.player.set_pause(1)
	def update_display(self):
		if self.queue_count == 0:
			text = f'🎵 内置音乐: {self.current_song}' if IS_PLAYING_LOCAL_MUSIC else f'🎵 最后一首: {self.current_song}'
		elif self.queue_count == 1:
			text = f'🎵 {self.current_song}\n2. {self.next_song}'
		elif self.queue_count == 2:
			text = f'🎵 {self.current_song}\n2. {self.next_song}\n3. {self.last_song}'
		else:
			text = f'🎵 {self.current_song}\n2. {self.next_song}\n......\n{self.queue_count + 1}. {self.last_song}'
		self.status_label.config(text=text)
		self.system_status_label.config(text=f"{self.system_status}")
		self.root.update_idletasks()
		width = self.root.winfo_reqwidth()
		height = self.root.winfo_reqheight()
		self.root.geometry(f"{width}x{height}")
	def set_status(self, current_song=None, next_song=None, queue_count=None, last_song=None, system_status=None):
		if current_song is not None: self.current_song = current_song
		if next_song is not None: self.next_song = next_song
		if queue_count is not None: self.queue_count = queue_count
		if last_song is not None: self.last_song = last_song
		if system_status is not None: self.system_status = system_status
		self.update_display()
	def start(self):
		self.update_display()
		def handle_interrupt(e=None):
			self.root.quit()
			self.root.destroy()
			os._exit(0)
		self.root.bind('<Control-c>', handle_interrupt)
		self.root.bind('<Escape>', handle_interrupt)
		self.root.mainloop()
def extract_song_name(input_text):
	try:
		keyword_positions = []
		for keyword in ["点歌"]:
			pos = input_text.find(keyword)
			if pos != -1:
				keyword_positions.append((pos, keyword))
		if not keyword_positions:
			return None
		last_keyword_pos, keyword = max(keyword_positions, key=lambda x: x[0])
		song_part = input_text[last_keyword_pos + len(keyword):]
		if not song_part:
			return None
		match = re.search(r'[^ ,，、.。!！?？;；:："＂\'＇`｀*＊+#＋\-－=＝_＿|｜~～@＃$＄%％^＆&()（）\[\]【】{}｛｝<>＜＞\s\t]', song_part)
		return song_part[match.start():] if match else None
	except Exception:
		return None
def search_music(song_name):
	params = {"word": song_name}
	for attempt in range(5):
		try:
			global_gui.set_status(system_status=f"🔍阅卷中...{song_name}({attempt+1}/5)")
			response = requests.get("https://api.vkeys.cn/v2/music/tencent", params=params, timeout=10)
			response.raise_for_status()
			result = response.json()
			if result.get("code") == 200:
				return result
			else:
				error_msg = f"❌阅卷失败: {result.get('message', '未知错误')}"
				global_gui.set_status(system_status=error_msg)
				if attempt < 2:
					time.sleep(1)
					continue
		except requests.exceptions.RequestException as e:
			error_msg = f"🌐网络断连: {str(e)}"
			global_gui.set_status(system_status=error_msg)
			if attempt < 2:
				time.sleep(1)
		except Exception as e:
			error_msg = f"🤯阅卷异常: {str(e)}"
			global_gui.set_status(system_status=error_msg)
			if attempt < 2:
				time.sleep(1)
	global_gui.set_status(system_status="📝❌答题卡作废，请重填")
	return None
def get_play_url(mid, song_name):
	params = {"mid": mid}
	for attempt in range(5):
		try:
			global_gui.set_status(system_status=f"📢调取广播...{song_name}({attempt+1}/5)")
			response = requests.get("https://api.vkeys.cn/v2/music/tencent", params=params, timeout=10)
			response.raise_for_status()
			result = response.json()
			if result.get("code") == 200:
				return result
			else:
				error_msg = f"📢调取广播失败: {result.get('message', '未知错误')}"
				global_gui.set_status(system_status=error_msg)
				if attempt < 2:
					time.sleep(1)
					continue
		except requests.exceptions.RequestException as e:
			error_msg = f"📡广播网络异常: {str(e)}"
			global_gui.set_status(system_status=error_msg)
			if attempt < 2:
				time.sleep(1)
		except Exception as e:
			error_msg = f"⚡广播调取异常: {str(e)}"
			global_gui.set_status(system_status=error_msg)
			if attempt < 2:
				time.sleep(1)
	global_gui.set_status(system_status="📢广播调取失败，请重新点歌")
	return None
class LiveConnection:
	def __init__(self, room_id, credential):
		self.room_id = room_id
		self.credential = credential
		self.last_event_time = 0
		self.room_danmaku = None
	async def connect_once(self):
		try:
			print(f"尝试连接直播间 {self.room_id}...")
			self.room_danmaku = live.LiveDanmaku(self.room_id, credential=self.credential)
			self.last_event_time = time.time()
			@self.room_danmaku.on('DANMU_MSG')
			async def on_danmaku(e):
				try:
					info = e['data']['info']
					user_info = info[2][1]
					danmaku_text = info[1]
					print(f"[弹幕] {user_info}: {danmaku_text}")
					global_gui.danmu_label.config(text=f"{user_info}: {danmaku_text}")
					if any(cmd in danmaku_text for cmd in ["点歌", "切歌", "测试"]):
						danmaku_queue.put((user_info, danmaku_text))
						sd.play(np.sin(2*np.pi*450*np.arange(8000)/8000), 8000)
				except Exception as e:
					print(f"解析弹幕出错: {e}")
				self.last_event_time = time.time()
			@self.room_danmaku.on('DISCONNECT')
			async def on_disconnect(e):
				global_gui.set_status(system_status=f"[看门猫🐱]连接断开")
				return
			@self.room_danmaku.on('ALL')
			async def on_all(e):
				print(f"事件{e}")
				self.last_event_time = time.time()
			@self.room_danmaku.on('TIMEOUT')
			async def on_timeout(e):
				global_gui.set_status(system_status=f"[看门猫🐱]心跳超时")
				return
			@self.room_danmaku.on('VERIFICATION_SUCCESSFUL')
			async def on_connect(e):
				global_gui.set_status(system_status=f"弹幕接收就绪")
				self.last_event_time = time.time()
			@self.room_danmaku.on('LIKE_INFO_V3_CLICK')
			async def on_LIKE_INFO_V3_CLICK(e):
				global_gui.set_status(system_status=f"{e['data']['data']['uinfo']['base']['name']} {e['data']['data']['like_text']}")
			@self.room_danmaku.on('SEND_GIFT')
			async def on_gift(e):
				global_gui.set_status(system_status=f"{e['data']['uname']} 赠送了 {e['data']['giftName']} x{e['data']['num']}")
			@self.room_danmaku.on('SPECIAL_GIFT')
			async def on_special_gift(e):
				global_gui.set_status(system_status=f"特殊礼物: {e['data']['uname']} 赠送了 {e['data']['giftName']}")
			await self.room_danmaku.connect()
		except Exception as e:
			print(f"连接失败: {e}")
			return
	async def monitor_events(self):
		while True:
			await asyncio.sleep(5)
			current_time = time.time()
			if current_time - self.last_event_time > 30:
				print("30秒内未接收到事件，连接可能已失效")
				return
	async def single_connection_attempt(self):
		connect_task = asyncio.create_task(self.connect_once())
		monitor_task = asyncio.create_task(self.monitor_events())
		done, pending = await asyncio.wait(
			[connect_task, monitor_task],
			return_when=asyncio.FIRST_COMPLETED
		)
		for task in pending:
			task.cancel()
		if self.room_danmaku:
			try:
				await self.room_danmaku.disconnect()
			except:
				pass
			self.room_danmaku = None
async def danmaku_main(config, credential):
	while True:
		try:
			await LiveConnection(config['roomId'], credential).single_connection_attempt()
		except Exception as e:
			global_gui.set_status(system_status=f"[看门猫🐱]连接异常: {e}")
		global_gui.set_status(system_status="[看门猫🐱]重启弹幕连接")
		sd.play(np.sign(np.sin(2*np.pi*450*np.arange(44100)/44100)), 44100)
		await asyncio.sleep(3)
class MusicPlayer:
	def __init__(self, music_folders):
		global IS_PLAYING_LOCAL_MUSIC
		self.music_folders = [Path(folder) for folder in music_folders]
		try:
			self.instance = vlc.Instance(['--no-video', '-I', 'dummy', '--quiet'])
			self.player = self.instance.media_player_new()
		except Exception as e:
			error_msg = f"🎵广播室开不了门: {str(e)}"
			sd.play(np.sign(np.sin(2*np.pi*500*np.arange(44100)/44100)), 44100)
			global_gui.set_status(system_status=error_msg)
			raise
		self.playlist = []
		self.is_idle = True
		self.current_media = None
		self.current_song_name = ""
		self.running = True
		self.idle_files = []
		self._load_local_files()
		try:
			self.watchdog = Watchdog(self)
			self.watchdog.start()
			self.play_thread = threading.Thread(target=self._play_worker, daemon=True)
			self.play_thread.start()
			self.danmaku_thread = threading.Thread(target=self._danmaku_input_worker, daemon=True)
			self.danmaku_thread.start()
		except Exception as e:
			error_msg = f"线程启动失败: {str(e)}"
			sd.play(np.sign(np.sin(2*np.pi*750*np.arange(44100)/44100)), 44100)
			global_gui.set_status(system_status=error_msg)
			self.running = False
			raise
	def _danmaku_input_worker(self):
		while self.running:
			try:
				try:
					item = danmaku_queue.get(timeout=1)
					if item:
						username, text = item
						self._process_command(username, text)
				except queue.Empty:
					pass
			except Exception as e:
				time.sleep(1)
	def _load_local_files(self):
		try:
			available_folder = None
			for folder in self.music_folders:
				if folder.exists():
					available_folder = folder
					break
			if available_folder is None:
				global_gui.set_status(system_status="📁所有考场默认音乐都不存在")
				return
			all_files = []
			for file_path in available_folder.rglob('*'):
				if file_path.is_file():
					all_files.append(file_path)
			self.idle_files = all_files
			if not self.idle_files:
				global_gui.set_status(system_status=f"📁考场无默认音乐: {available_folder}")
			else:
				global_gui.set_status(system_status=f"📁已加载本地音乐: {available_folder}")
		except Exception as e:
			global_gui.set_status(system_status=f"📁加载本地音乐失败: {str(e)}")
			self.idle_files = []
	def _get_random_file(self):
		if not self.idle_files:
			return None, ""
		file_path = random.choice(self.idle_files)
		return file_path, file_path.name
	def _play_media_with_retry(self, media_path, song_name, max_retries=5):
		global IS_PLAYING_LOCAL_MUSIC
		for attempt in range(max_retries):
			try:
				if isinstance(media_path, Path):
					media = vlc.Media(media_path.as_uri())
					display_name = media_path.name
					IS_PLAYING_LOCAL_MUSIC = True
				else:
					media = vlc.Media(media_path)
					display_name = song_name
					IS_PLAYING_LOCAL_MUSIC = False
				global_gui.set_status(current_song=display_name, system_status=f"🎧播放({attempt+1}/{max_retries})")
				self.player.set_media(media)
				self.player.play()
				time.sleep(0.5)
				state = self.player.get_state()
				if state in [vlc.State.Playing, vlc.State.Opening]:
					self.current_song_name = display_name
					return True
				elif state in [vlc.State.Error, vlc.State.Stopped]:
					error_msg = f"🔇播放失败({attempt+1}/{max_retries})"
					global_gui.set_status(system_status=error_msg)
					if attempt < max_retries - 1:
						time.sleep(1)
					continue
			except Exception as e:
				error_msg = f"❌播放错误({attempt+1}/{max_retries}): {str(e)}"
				global_gui.set_status(system_status=error_msg)
				if attempt < max_retries - 1:
					time.sleep(1)
		global_gui.set_status(system_status="⏭️播放失败，跳过该歌曲")
		return False
	def _play_worker(self):
		while self.running:
			try:
				if self.playlist:
					self.is_idle = False
					song_name, media_path = self.playlist.pop(0)
					queue_info = self._get_queue_info()
					global_gui.set_status(**queue_info)
					if not self._play_media_with_retry(media_path, song_name):
						continue
					while (self.running and self.player.get_state() not in [vlc.State.Ended, vlc.State.Stopped, vlc.State.Error]):
						time.sleep(0.5)
				else:
					self.is_idle = True
					if self.idle_files:
						media_path, song_name = self._get_random_file()
						if media_path:
							global_gui.set_status(current_song=song_name, next_song="", queue_count=0, last_song="")
							if not self._play_media_with_retry(media_path, song_name):
								continue
							while (self.running and self.player.get_state() not in [vlc.State.Ended, vlc.State.Stopped, vlc.State.Error]):
								time.sleep(0.5)
					else:
						time.sleep(1)
			except Exception as e:
				error_msg = f"❌播放工作线程错误: {str(e)}"
				global_gui.set_status(system_status=error_msg)
				time.sleep(1)
	def _get_queue_info(self):
		queue_count = len(self.playlist)
		if queue_count == 0:
			return {'current_song': self.current_song_name, 'next_song': "", 'queue_count': 0, 'last_song': ""}
		elif queue_count == 1:
			next_song_name, _ = self.playlist[0]
			return {'current_song': self.current_song_name, 'next_song': next_song_name, 'queue_count': 1, 'last_song': next_song_name}
		else:
			next_song_name, _ = self.playlist[0]
			last_song_name, _ = self.playlist[-1]
			return {'current_song': self.current_song_name, 'next_song': next_song_name, 'queue_count': queue_count, 'last_song': last_song_name}
	def _process_command(self, username, content):
		try:
			if "切歌" in content:
				global_gui.set_status(system_status="💨跳过题目")
				self.player.stop()
				return True
			song_name = extract_song_name(content)
			if song_name:
				global_gui.set_status(system_status=f"🔍审题: {song_name}")
				search_result = search_music(song_name)
				if not search_result or search_result.get("code") != 200:
					global_gui.set_status(system_status="🌪️😵💫哎呀,点歌系统被难倒了")
					return False
				data = search_result.get("data", [])
				if not data:
					global_gui.set_status(system_status="✨🤔你的关键词可能超纲了")
					return False
				first_song = data[0]
				song_title = first_song.get('song', '未知歌名')
				singer = first_song.get('singer', '未知歌手')
				play_result = get_play_url(first_song.get("mid"), f"{song_title} - {singer}")
				if not play_result or play_result.get("code") != 200 or not play_result.get("data", {}).get("url", ""):
					global_gui.set_status(system_status="📄答案神秘失踪")
					return False
				play_url = play_result.get("data", {}).get("url", "")
				self.playlist.append((f"{song_title} - {singer}", play_url))
				queue_info = self._get_queue_info()
				queue_info['system_status'] = f"✅点歌成功: {song_title}"
				global_gui.set_status(**queue_info)
				if self.is_idle and self.player.is_playing():
					self.player.stop()
				return True
			return False
		except Exception as e:
			error_msg = f"❌命令处理错误: {str(e)}"
			global_gui.set_status(system_status=error_msg)
			return False
	def stop(self):
		self.running = False
		if hasattr(self, 'watchdog'):
			self.watchdog.stop()
		try:
			self.player.stop()
		except Exception:
			pass
def start_danmaku_system(config):
	def run_async():
		try:
			credential = live.Credential(
				sessdata=config['sessdata'],
				bili_jct=config['bili_jct'],
				buvid3=config['buvid3']
			)
			loop = asyncio.new_event_loop()
			asyncio.set_event_loop(loop)
			loop.run_until_complete(danmaku_main(config, credential))
		except Exception as e:
			print(f"[看门猫🐱]弹幕系统错误: {e}")
			global_gui.set_status(system_status=f"[看门猫🐱]弹幕系统错误: {e}")
			time.sleep(3)
			start_danmaku_system(config)
	thread = threading.Thread(target=run_async, daemon=True)
	thread.start()
def main():
	global global_gui
	try:
		config_path = find_config_file()
		with open(config_path, 'r', encoding='utf-8') as f:
			config = json.load(f)
		gui = PointSongGUI()
		gui.set_status(current_song="", next_song="", queue_count=0, last_song="")
		def start_player():
			try:
				start_danmaku_system(config)
				player = MusicPlayer(music_folders)
				gui.player_ref = player
				while player.running:
					time.sleep(1)
			except Exception as e:
				global_gui.set_status(system_status=f"❌播放器错误: {str(e)}")
			finally:
				if 'player' in locals():
					player.stop()
		player_thread = threading.Thread(target=start_player, daemon=True)
		player_thread.start()
		gui.start()
	except Exception as e:
		global_gui.set_status(system_status=f"考场爆炸💥: {str(e)}")
		gui.start()
if __name__ == "__main__":
	main()
